package org.spiderflow.core.expression.parsing;


import org.spiderflow.core.expression.ExpressionError;
import org.spiderflow.core.expression.ExpressionTemplate;
import org.spiderflow.core.expression.parsing.ast.Expression;
import org.spiderflow.core.expression.parsing.ast.Node;
import org.spiderflow.core.expression.parsing.ast.Text;
import org.spiderflow.core.expression.parsing.ast.access.MapOrArrayAccess;
import org.spiderflow.core.expression.parsing.ast.access.MemberAccess;
import org.spiderflow.core.expression.parsing.ast.access.VariableAccess;
import org.spiderflow.core.expression.parsing.ast.call.FunctionCall;
import org.spiderflow.core.expression.parsing.ast.call.MethodCall;
import org.spiderflow.core.expression.parsing.ast.literal.BooleanLiteral;
import org.spiderflow.core.expression.parsing.ast.literal.ByteLiteral;
import org.spiderflow.core.expression.parsing.ast.literal.CharacterLiteral;
import org.spiderflow.core.expression.parsing.ast.literal.DoubleLiteral;
import org.spiderflow.core.expression.parsing.ast.literal.FloatLiteral;
import org.spiderflow.core.expression.parsing.ast.literal.IntegerLiteral;
import org.spiderflow.core.expression.parsing.ast.literal.ListLiteral;
import org.spiderflow.core.expression.parsing.ast.literal.LongLiteral;
import org.spiderflow.core.expression.parsing.ast.literal.MapLiteral;
import org.spiderflow.core.expression.parsing.ast.literal.NullLiteral;
import org.spiderflow.core.expression.parsing.ast.literal.ShortLiteral;
import org.spiderflow.core.expression.parsing.ast.literal.StringLiteral;
import org.spiderflow.core.expression.parsing.ast.operation.BinaryOperation;
import org.spiderflow.core.expression.parsing.ast.operation.TernaryOperation;
import org.spiderflow.core.expression.parsing.ast.operation.UnaryOperation;
import org.spiderflow.core.expression.parsing.token.Token;
import org.spiderflow.core.expression.parsing.token.TokenStream;
import org.spiderflow.core.expression.parsing.token.TokenType;
import org.spiderflow.core.expression.parsing.token.Tokenizer;

import javax.xml.transform.Source;
import java.util.ArrayList;
import java.util.List;


/**
 * Parses a {@link Source} into a {@link ExpressionTemplate}. The implementation is a simple recursive descent parser with a lookahead of
 * 1.
 *
 * @author zsh
 */
public class Parser {
    private Parser() {
    }

    /**
     * Parses a {@link Source} into a {@link ExpressionTemplate}.
     **/
    public static List<Node> parse(String source) {
        List<Node> nodes = new ArrayList<>();
        TokenStream stream = new TokenStream(new Tokenizer().split(source));
        while (stream.hasMore()) {
            nodes.add(parseStatement(stream));
        }
        return nodes;
    }

    /**
     * Parse a statement, which may either be a text block, if statement, for statement, while statement, macro definition,
     * include statement or an expression.
     **/
    private static Node parseStatement(TokenStream tokens) {
        Node result;

        if (tokens.match(TokenType.TEXT_BLOCK, false)) {
            result = new Text(tokens.consume().getSpan());
        } else {
            result = parseExpression(tokens);
        }

        // consume semi-colons as statement delimiters
        boolean b;
        do {
            b = tokens.match(";", true);
        } while (b);
        return result;
    }


    private static Expression parseExpression(TokenStream stream) {
        return parseTernaryOperator(stream);
    }

    private static Expression parseTernaryOperator(TokenStream stream) {
        Expression condition = parseBinaryOperator(stream, 0);
        if (stream.match(TokenType.QUESTION_MARK, true)) {
            Expression trueExpression = parseTernaryOperator(stream);
            stream.expect(TokenType.COLON);
            Expression falseExpression = parseTernaryOperator(stream);
            return new TernaryOperation(condition, trueExpression, falseExpression);
        } else {
            return condition;
        }
    }

    private static final TokenType[][] BINARY_OPERATOR_PRECEDENCE = new TokenType[][]{new TokenType[]{TokenType.ASSIGNMENT},
            new TokenType[]{TokenType.OR, TokenType.AND, TokenType.XOR}, new TokenType[]{TokenType.EQUAL, TokenType.NOT_EQUAL},
            new TokenType[]{TokenType.LESS, TokenType.LESS_EQUAL, TokenType.GREATER, TokenType.GREATER_EQUAL}, new TokenType[]{TokenType.PLUS, TokenType.MINUS},
            new TokenType[]{TokenType.FORWARD_SLASH, TokenType.ASTERISK, TokenType.PERCENTAGE}};

    private static Expression parseBinaryOperator(TokenStream stream, int level) {
        int nextLevel = level + 1;
        Expression left = nextLevel == BINARY_OPERATOR_PRECEDENCE.length ? parseUnaryOperator(stream) : parseBinaryOperator(stream, nextLevel);

        TokenType[] operators = BINARY_OPERATOR_PRECEDENCE[level];
        while (stream.hasMore() && stream.match(false, operators)) {
            Token operator = stream.consume();
            Expression right = nextLevel == BINARY_OPERATOR_PRECEDENCE.length ? parseUnaryOperator(stream) : parseBinaryOperator(stream, nextLevel);
            left = new BinaryOperation(left, operator, right);
        }

        return left;
    }

    private static final TokenType[] unaryOperators = new TokenType[]{TokenType.NOT, TokenType.PLUS, TokenType.MINUS};

    private static Expression parseUnaryOperator(TokenStream stream) {
        if (stream.match(false, unaryOperators)) {
            return new UnaryOperation(stream.consume(), parseUnaryOperator(stream));
        } else {
            if (stream.match(TokenType.LEFT_PARENTHESES, true)) {
                Expression expression = parseExpression(stream);
                stream.expect(TokenType.RIGHT_PARENTHESES);
                return expression;
            } else {
                return parseAccessOrCallOrLiteral(stream);
            }
        }
    }

    private static Expression parseAccessOrCallOrLiteral(TokenStream stream) {
        if (stream.match(TokenType.IDENTIFIER, false)) {
            return parseAccessOrCall(stream, TokenType.IDENTIFIER);
        } else if (stream.match(TokenType.LEFT_CURLY, false)) {
            return parseMapLiteral(stream);
        } else if (stream.match(TokenType.LEFT_BRACKET, false)) {
            return parseListLiteral(stream);
        } else if (stream.match(TokenType.STRING_LITERAL, false)) {
            return parseStringLiteral(stream);
        } else if (stream.match(TokenType.BOOLEAN_LITERAL, false)) {
            return new BooleanLiteral(stream.expect(TokenType.BOOLEAN_LITERAL).getSpan());
        } else if (stream.match(TokenType.DOUBLE_LITERAL, false)) {
            return new DoubleLiteral(stream.expect(TokenType.DOUBLE_LITERAL).getSpan());
        } else if (stream.match(TokenType.FLOAT_LITERAL, false)) {
            return new FloatLiteral(stream.expect(TokenType.FLOAT_LITERAL).getSpan());
        } else if (stream.match(TokenType.BYTE_LITERAL, false)) {
            return new ByteLiteral(stream.expect(TokenType.BYTE_LITERAL).getSpan());
        } else if (stream.match(TokenType.SHORT_LITERAL, false)) {
            return new ShortLiteral(stream.expect(TokenType.SHORT_LITERAL).getSpan());
        } else if (stream.match(TokenType.INTEGER_LITERAL, false)) {
            return new IntegerLiteral(stream.expect(TokenType.INTEGER_LITERAL).getSpan());
        } else if (stream.match(TokenType.LONG_LITERAL, false)) {
            return new LongLiteral(stream.expect(TokenType.LONG_LITERAL).getSpan());
        } else if (stream.match(TokenType.CHARACTER_LITERAL, false)) {
            return new CharacterLiteral(stream.expect(TokenType.CHARACTER_LITERAL).getSpan());
        } else if (stream.match(TokenType.NULL_LITERAL, false)) {
            return new NullLiteral(stream.expect(TokenType.NULL_LITERAL).getSpan());
        } else {
            throw ExpressionError.error(stream, "Expected a variable, field, map, array, function or method call, or literal.");
        }
    }

    private static Expression parseStringLiteral(TokenStream stream) {
        if (stream.hasNext()) {
            if (stream.next().getType() == TokenType.PERIOD) {
                stream.prev();
                return parseAccessOrCall(stream, TokenType.STRING_LITERAL);
            }
            stream.prev();
        }
        return new StringLiteral(stream.expect(TokenType.STRING_LITERAL).getSpan());
    }

    private static Expression parseMapLiteral(TokenStream stream) {
        Span openCurly = stream.expect(TokenType.LEFT_CURLY).getSpan();

        List<Token> keys = new ArrayList<>();
        List<Expression> values = new ArrayList<>();
        while (stream.hasMore() && !stream.match("}", false)) {
            if (stream.match(TokenType.STRING_LITERAL, false)) {
                keys.add(stream.expect(TokenType.STRING_LITERAL));
            } else {
                keys.add(stream.expect(TokenType.IDENTIFIER));
            }

            stream.expect(":");
            values.add(parseExpression(stream));
            if (!stream.match("}", false)) {
                stream.expect(TokenType.COMMA);
            }
        }
        Span closeCurly = stream.expect("}").getSpan();
        return new MapLiteral(new Span(openCurly, closeCurly), keys, values);
    }

    private static Expression parseListLiteral(TokenStream stream) {
        Span openBracket = stream.expect(TokenType.LEFT_BRACKET).getSpan();

        List<Expression> values = new ArrayList<>();
        while (stream.hasMore() && !stream.match(TokenType.RIGHT_BRACKET, false)) {
            values.add(parseExpression(stream));
            if (!stream.match(TokenType.RIGHT_BRACKET, false)) {
                stream.expect(TokenType.COMMA);
            }
        }

        Span closeBracket = stream.expect(TokenType.RIGHT_BRACKET).getSpan();
        return new ListLiteral(new Span(openBracket, closeBracket), values);
    }

    private static Expression parseAccessOrCall(TokenStream stream, TokenType tokenType) {
        Span identifier = stream.expect(tokenType).getSpan();
        Expression result = tokenType == TokenType.STRING_LITERAL ? new StringLiteral(identifier) : new VariableAccess(identifier);

        while (stream.hasMore() && stream.match(false, TokenType.LEFT_PARENTHESES, TokenType.LEFT_BRACKET, TokenType.PERIOD)) {

            // function or method call
            if (stream.match(TokenType.LEFT_PARENTHESES, false)) {
                List<Expression> arguments = parseArguments(stream);
                Span closingSpan = stream.expect(TokenType.RIGHT_PARENTHESES).getSpan();
                if (result instanceof VariableAccess || result instanceof MapOrArrayAccess) {
                    result = new FunctionCall(new Span(result.getSpan(), closingSpan), result, arguments);
                } else if (result instanceof MemberAccess) {
                    result = new MethodCall(new Span(result.getSpan(), closingSpan), (MemberAccess) result, arguments);
                } else {
                    throw ExpressionError.error(stream, "Expected a variable, field or method.");
                }
            }

            // map or array access
            else if (stream.match(TokenType.LEFT_BRACKET, true)) {
                Expression keyOrIndex = parseExpression(stream);
                Span closingSpan = stream.expect(TokenType.RIGHT_BRACKET).getSpan();
                result = new MapOrArrayAccess(new Span(result.getSpan(), closingSpan), result, keyOrIndex);
            }

            // field or method access
            else if (stream.match(TokenType.PERIOD, true)) {
                identifier = stream.expect(TokenType.IDENTIFIER).getSpan();
                result = new MemberAccess(result, identifier);
            }
        }

        return result;
    }

    /**
     * Does not consume the closing parentheses.
     **/
    private static List<Expression> parseArguments(TokenStream stream) {
        stream.expect(TokenType.LEFT_PARENTHESES);
        List<Expression> arguments = new ArrayList<>();
        while (stream.hasMore() && !stream.match(TokenType.RIGHT_PARENTHESES, false)) {
            arguments.add(parseExpression(stream));
            if (!stream.match(TokenType.RIGHT_PARENTHESES, false)) {
                stream.expect(TokenType.COMMA);
            }
        }
        return arguments;
    }
}
